/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2002-2006
* Sleepycat Software. All rights reserved.
*
* $Id: Locker.java,v 1.1 2006/05/06 08:59:05 ckaestne Exp $
*/
package com.sleepycat.je.txn;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.DeadlockException;
import com.sleepycat.je.DbInternal;
import com.sleepycat.je.LockNotGrantedException;
import com.sleepycat.je.LockStats;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.dbi.CursorImpl;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.tree.BIN;
import com.sleepycat.je.tree.BINReference;
import com.sleepycat.je.tree.Key;
/**
* Locker instances are JE's route to locking and transactional support. This
* class is the abstract base class for BasicLocker, ThreadLocker, Txn and
* AutoTxn. Locker instances are in fact only a transaction shell to get to
* the lock manager, and don't guarantee transactional semantics. Txn and
* AutoTxn instances are both truely transactional, but have different ending
* behaviors.
*/
public abstract class Locker {
private static final String DEBUG_NAME = Locker.class.getName();
protected EnvironmentImpl envImpl;
protected LockManager lockManager;
protected long id; // transaction id
protected boolean readUncommittedDefault; // read-uncommitted is default
/* Timeouts */
protected boolean defaultNoWait; // true for non-blocking
protected long lockTimeOutMillis; // timeout period for lock, in ms
private long txnTimeOutMillis; // timeout period for txns, in ms
private long txnStartMillis; // for txn timeout determination
private Lock waitingFor; // The lock that this txn is
// waiting for.
/*
* DeleteInfo refers to BINReferences that should be sent to the
* INCompressor for asynchronous compressing after the transaction ends.
*/
protected Map deleteInfo;
/*
* To support handle lock transfers, each txn keeps maps handle locks to
* database handles. This is maintained as a map where the key is the
* handle lock id and the value is a set of database handles that
* correspond to that handle lock. This is a 1 - many relationship because
* a single handle lock can cover multiple database handles opened by the
* same transaction.
*/
protected Map handleLockToHandleMap; // 1-many, used for commits
protected Map handleToHandleLockMap; // 1-1, used for aborts
/**
* The thread that created this locker. Used for debugging, and by the
* ThreadLocker subclass. Note that thread may be null if the Locker is
* instantiated by reading the log.
*/
protected Thread thread;
/**
* Create a locker id. This constructor is called very often, so it should
* be as streamlined as possible.
*
* @param lockManager lock manager for this environment
* @param readUncommittedDefault if true, this transaction does
* read-uncommitted by default
* @param noWait if true, non-blocking lock requests are used.
*/
public Locker(EnvironmentImpl envImpl,
boolean readUncommittedDefault,
boolean noWait)
throws DatabaseException {
TxnManager txnManager = envImpl.getTxnManager();
this.id = generateId(txnManager);
this.envImpl = envImpl;
lockManager = txnManager.getLockManager();
this.readUncommittedDefault = readUncommittedDefault;
this.waitingFor = null;
/* get the default lock timeout. */
defaultNoWait = noWait;
lockTimeOutMillis = envImpl.getLockTimeout();
/*
* Check the default txn timeout. If non-zero, remember the txn start
* time.
*/
txnTimeOutMillis = envImpl.getTxnTimeout();
if (txnTimeOutMillis != 0) {
txnStartMillis = System.currentTimeMillis();
} else {
txnStartMillis = 0;
}
/* Save the thread used to create the locker. */
thread = Thread.currentThread();
/*
* Do lazy initialization of deleteInfo and handle lock maps, to
* conserve memory.
*/
}
/**
* For reading from the log.
*/
Locker() {
}
/**
* A Locker has to generate its next id. Some subtypes, like BasicLocker,
* have a single id for all instances because they are never used for
* recovery. Other subtypes ask the txn manager for an id.
*/
protected abstract long generateId(TxnManager txnManager);
/**
* @return the transaction's id.
*/
public long getId() {
return id;
}
/**
* @return the default no-wait (non-blocking) setting.
*/
public boolean getDefaultNoWait() {
return defaultNoWait;
}
/**
* Get the lock timeout period for this transaction, in milliseconds
*/
public synchronized long getLockTimeout() {
return lockTimeOutMillis;
}
/**
* Set the lock timeout period for any locks in this transaction,
* in milliseconds.
*/
public synchronized void setLockTimeout(long timeOutMillis) {
lockTimeOutMillis = timeOutMillis;
}
/**
* Set the timeout period for this transaction, in milliseconds.
*/
public synchronized void setTxnTimeout(long timeOutMillis) {
txnTimeOutMillis = timeOutMillis;
txnStartMillis = System.currentTimeMillis();
}
/**
* @return true if transaction was created with read-uncommitted as a
* default.
*/
public boolean isReadUncommittedDefault() {
return readUncommittedDefault;
}
Lock getWaitingFor() {
return waitingFor;
}
void setWaitingFor(Lock lock) {
waitingFor = lock;
}
/**
* Set the state of a transaction to ONLY_ABORTABLE.
*/
void setOnlyAbortable() {
/* no-op unless Txn. */
}
protected abstract void checkState(boolean ignoreCalledByAbort)
throws DatabaseException;
/*
* Obtain and release locks.
*/
/**
* Abstract method to a blocking or non-blocking lock of the given type on
* the given nodeId. Unlike the lock() method, this method does not throw
* LockNotGrantedException and can therefore be used by nonBlockingLock to
* probe for a lock without the overhead of an exception stack trace.
*
* @param nodeId is the node to lock.
*
* @param lockType is the type of lock to request.
*
* @param noWait is true to override the defaultNoWait setting. If true,
* or if defaultNoWait is true, throws LockNotGrantedException if the lock
* cannot be granted without waiting.
*
* @param database is the database containing nodeId.
*
* @throws DeadlockException if acquiring a blocking lock would result in a
* deadlock.
*/
abstract LockResult lockInternal(long nodeId,
LockType lockType,
boolean noWait,
DatabaseImpl database)
throws DeadlockException, DatabaseException;
/**
* Request a blocking or non-blocking lock of the given type on the given
* nodeId.
*
* @param nodeId is the node to lock.
*
* @param lockType is the type of lock to request.
*
* @param noWait is true to override the defaultNoWait setting. If true,
* or if defaultNoWait is true, throws LockNotGrantedException if the lock
* cannot be granted without waiting.
*
* @param database is the database containing nodeId.
*
* @throws LockNotGrantedException if a non-blocking lock was denied.
*
* @throws DeadlockException if acquiring a blocking lock would result in a
* deadlock.
*/
public LockResult lock(long nodeId,
LockType lockType,
boolean noWait,
DatabaseImpl database)
throws LockNotGrantedException, DeadlockException, DatabaseException {
LockResult result = lockInternal(nodeId, lockType, noWait, database);
if (result.getLockGrant() == LockGrantType.DENIED) {
/* DENIED can only be returned for a non-blocking lock. */
throw new LockNotGrantedException("Non-blocking lock was denied.");
} else {
return result;
}
}
/**
* Request a non-blocking lock of the given type on the given nodeId.
*
* <p>Unlike lock(), this method returns LockGrantType.DENIED if the lock
* is denied rather than throwing LockNotGrantedException. This method
* should therefore not be used as the final lock for a user operation,
* since in that case LockNotGrantedException should be thrown for a denied
* lock. It is normally used only to probe for a lock, and other recourse
* is taken if the lock is denied.</p>
*
* @param nodeId is the node to lock.
*
* @param lockType is the type of lock to request.
*
* @param database is the database containing nodeId.
*/
public LockResult nonBlockingLock(long nodeId,
LockType lockType,
DatabaseImpl database)
throws DatabaseException {
return lockInternal(nodeId, lockType, true, database);
}
/**
* Release the lock on this LN and remove from the transaction's owning
* set.
*/
public void releaseLock(long nodeId)
throws DatabaseException {
/*
* If successful, the lock manager will call back to the transaction
* and remove the lock from the lock collection. Done this way because
* we can't get a handle on the lock without creating another object
* XXX: bite the bullet, new a holder object, pass it back?
*/
lockManager.release(nodeId, this);
}
/**
* Revert this lock from a write lock to a read lock.
*/
public void demoteLock(long nodeId)
throws DatabaseException {
/*
* If successful, the lock manager will call back to the transaction
* and adjust the location of the lock in the lock collection.
*/
lockManager.demote(nodeId, this);
}
/**
* Returns whether this locker is transactional.
*/
public abstract boolean isTransactional();
/**
* Returns whether the isolation level of this locker is serializable.
*/
public abstract boolean isSerializableIsolation();
/**
* Returns whether the isolation level of this locker is read-committed.
*/
public abstract boolean isReadCommittedIsolation();
/**
* Returns the underlying Txn if the locker is transactional, or null if
* the locker is non-transactional. For a Txn-based locker, this method
* returns 'this'. For a BuddyLocker, this method may returns the buddy.
*/
public abstract Txn getTxnLocker();
/**
* Creates a fresh non-transactional locker, while retaining any
* transactional locks held by this locker. This method is called when the
* cursor for this locker is cloned.
*
* <p>In general, transactional lockers return 'this' when this method is
* called, while non-transactional lockers return a new instance.</p>
*/
public abstract Locker newNonTxnLocker()
throws DatabaseException;
/**
* Releases any non-transactional locks held by this locker. This method
* is called when the cursor moves to a new position or is closed.
*
* <p>In general, transactional lockers do nothing when this method is
* called, while non-transactional lockers release all locks as if
* operationEnd were called.</p>
*/
public abstract void releaseNonTxnLocks()
throws DatabaseException;
/**
* Returns whether this locker can share locks with the given locker.
*
* <p>All lockers share locks with a BuddyLocker whose buddy is this
* locker. To support BuddyLocker when overriding this method, always
* return true if this implementation (super.sharesLocksWith(...)) returns
* true.</p>
*/
public boolean sharesLocksWith(Locker other) {
if (other instanceof BuddyLocker) {
BuddyLocker buddy = (BuddyLocker) other;
return buddy.getBuddy() == this;
} else {
return false;
}
}
/**
* The equivalent of calling operationEnd(true).
*/
public abstract void operationEnd()
throws DatabaseException;
/**
* Different types of transactions do different things when the operation
* ends. Txns do nothing, AutoTxns commit or abort, and BasicLockers and
* ThreadLockers just release locks.
*
* @param operationOK is whether the operation succeeded, since
* that may impact ending behavior. (i.e for AutoTxn)
*/
public abstract void operationEnd(boolean operationOK)
throws DatabaseException;
/**
* We're at the end of an operation. Move this handle lock to the
* appropriate owner.
*/
public abstract void setHandleLockOwner(boolean operationOK,
Database dbHandle,
boolean dbIsClosing)
throws DatabaseException;
/**
* A SUCCESS status equals operationOk.
*/
public void operationEnd(OperationStatus status)
throws DatabaseException {
operationEnd(status == OperationStatus.SUCCESS);
}
/**
* Tell this transaction about a cursor.
*/
public abstract void registerCursor(CursorImpl cursor)
throws DatabaseException;
/**
* Remove a cursor from this txn.
*/
public abstract void unRegisterCursor(CursorImpl cursor)
throws DatabaseException;
/*
* Transactional support
*/
/**
* @return the abort LSN for this node.
*/
public abstract long getAbortLsn(long nodeId)
throws DatabaseException;
/**
* @return the WriteLockInfo for this node.
*/
public abstract WriteLockInfo getWriteLockInfo(long nodeId)
throws DatabaseException;
/**
* Database operations like remove and truncate leave behind
* residual DatabaseImpls that must be purged at transaction
* commit or abort.
*/
public abstract void markDeleteAtTxnEnd(DatabaseImpl db,
boolean deleteAtCommit)
throws DatabaseException;
/**
* Add delete information, to be added to the inCompressor queue
* when the transaction ends.
*/
public void addDeleteInfo(BIN bin, Key deletedKey)
throws DatabaseException {
synchronized (this) {
/* Maintain only one binRef per node. */
if (deleteInfo == null) {
deleteInfo = new HashMap();
}
Long nodeId = new Long(bin.getNodeId());
BINReference binRef = (BINReference) deleteInfo.get(nodeId);
if (binRef == null) {
binRef = bin.createReference();
deleteInfo.put(nodeId, binRef);
}
binRef.addDeletedKey(deletedKey);
}
}
/*
* Manage locks owned by this transaction. Note that transactions that will
* be multithreaded must override these methods and provide synchronized
* implementations.
*/
/**
* Add a lock to set owned by this transaction.
*/
abstract void addLock(Long nodeId,
Lock lock,
LockType type,
LockGrantType grantStatus)
throws DatabaseException;
/**
* @return true if this transaction created this node,
* for a operation with transactional semantics.
*/
public abstract boolean createdNode(long nodeId)
throws DatabaseException;
/**
* Remove the lock from the set owned by this transaction. If specified to
* LockManager.release, the lock manager will call this when its releasing
* a lock.
*/
abstract void removeLock(long nodeId, Lock lock)
throws DatabaseException;
/**
* A lock is being demoted. Move it from the write collection into the read
* collection.
*/
abstract void moveWriteToReadLock(long nodeId, Lock lock);
/**
* Get lock count, for per transaction lock stats, for internal debugging.
*/
public abstract LockStats collectStats(LockStats stats)
throws DatabaseException;
/*
* Check txn timeout, if set. Called by the lock manager when blocking on a
* lock.
*/
boolean isTimedOut()
throws DatabaseException {
if (txnStartMillis != 0) {
long diff = System.currentTimeMillis() - txnStartMillis;
if (diff > txnTimeOutMillis) {
return true;
}
}
return false;
}
/* public for jca/ra/JELocalTransaction. */
public long getTxnTimeOut() {
return txnTimeOutMillis;
}
long getTxnStartMillis() {
return txnStartMillis;
}
/**
* Remove this Database from the protected Database handle set
*/
void unregisterHandle(Database dbHandle) {
/*
* handleToHandleLockMap may be null if the db handle was never really
* added. This might be the case because of an unregisterHandle that
* comes from a finally clause, where the db handle was never
* successfully opened.
*/
if (handleToHandleLockMap != null) {
handleToHandleLockMap.remove(dbHandle);
}
}
/**
* Remember how handle locks and handles match up.
*/
public void addToHandleMaps(Long handleLockId,
Database databaseHandle) {
// TODO: mutex after measurement
Set dbHandleSet = null;
if (handleLockToHandleMap == null) {
/*
* We do lazy initialization of the maps, since they're used
* infrequently.
*/
handleLockToHandleMap = new Hashtable();
handleToHandleLockMap = new Hashtable();
} else {
dbHandleSet = (Set) handleLockToHandleMap.get(handleLockId);
}
if (dbHandleSet == null) {
dbHandleSet = new HashSet();
handleLockToHandleMap.put(handleLockId, dbHandleSet);
}
/* Map handle lockIds -> 1 or more database handles. */
dbHandleSet.add(databaseHandle);
/* Map database handles -> handle lock id */
handleToHandleLockMap.put(databaseHandle, handleLockId);
}
/**
* @return true if this txn is willing to give up the handle lock to
* another txn before this txn ends.
*/
public boolean isHandleLockTransferrable() {
return true;
}
/**
* The currentTxn passes responsiblity for this db handle lock to a txn
* owned by the Database object.
*/
void transferHandleLockToHandle(Database dbHandle)
throws DatabaseException {
/*
* Transfer responsiblity for this db lock from this txn to a new
* protector.
*/
Locker holderTxn = new BasicLocker(envImpl);
transferHandleLock(dbHandle, holderTxn, true );
}
/**
*
*/
public void transferHandleLock(Database dbHandle,
Locker destLocker,
boolean demoteToRead)
throws DatabaseException {
/*
* Transfer responsiblity for dbHandle's handle lock from this txn to
* destLocker. If the dbHandle's databaseImpl is null, this handle
* wasn't opened successfully.
*/
if (DbInternal.dbGetDatabaseImpl(dbHandle) != null) {
Long handleLockId = (Long) handleToHandleLockMap.get(dbHandle);
if (handleLockId != null) {
/* We have a handle lock for this db. */
long nodeId = handleLockId.longValue();
/* Move this lock to the destination txn. */
lockManager.transfer(nodeId, this, destLocker, demoteToRead);
/*
* Make the destination txn remember that it now owns this
* handle lock.
*/
destLocker.addToHandleMaps(handleLockId, dbHandle);
/* Take this out of the handle lock map. */
Set dbHandleSet = (Set)
handleLockToHandleMap.get(handleLockId);
Iterator iter = dbHandleSet.iterator();
while (iter.hasNext()) {
if (((Database) iter.next()) == dbHandle) {
iter.remove();
break;
}
}
if (dbHandleSet.size() == 0) {
handleLockToHandleMap.remove(handleLockId);
}
/*
* This Database must remember what txn owns it's handle lock.
*/
DbInternal.dbSetHandleLocker(dbHandle, destLocker);
}
}
}
/**
* If necessary, remember that this txn once owned a handle lock. Done to
* make commit optimizations work correctly.
*/
protected void rememberHandleWriteLock(Long lockId) {
/* by default, nothing to do. */
}
/*
* Helpers
*/
public String toString() {
String className = getClass().getName();
className = className.substring(className.lastIndexOf('.') + 1);
return Long.toString(id) + "_" +
((thread == null) ? "" : thread.getName()) + "_" +
className;
}
/**
* Dump lock table, for debugging
*/
public void dumpLockTable()
throws DatabaseException {
lockManager.dump();
}
}